Loopback Generator

Published: 2025-05-29

I have been teaching myself Golang over the last year or so. A big part of my learning has been through a peronal project I call Gollenet where I build a model of a "large" ISP network in EVE-NG. The number of routers in this project grew so quickly that I needed a way to automate the configuration of all devices in the topology. Enter Golang.

One part of the project is to automate the generation of loopbacks and linknets in my topology. Every core or PE router need its own loopback IP-address; every link needs its own /31 IPv4 subnet. I have written a few versions of this "Gollenet-confgen" tool now and while working on v3 yesterday I stumbled on a really neat way to generate loopbacks. In this article I will be walking you through the code.

internal/prefix/prefix.go:

package prefix

import (
    "fmt"
    "net/netip"
)

type Service struct {
    NextCoreLoopback func() (netip.Addr, error)
    NextPeerLoopback func() (netip.Addr, error)
    NextEdgeLoopback func() (netip.Addr, error)
}

func NewService() (Service, error) {

    coreRange, _ := netip.ParsePrefix("10.0.0.0/24")
    peerRange, _ := netip.ParsePrefix("10.0.1.0/24")
    edgeRange, _ := netip.ParsePrefix("10.0.2.0/24")

    s := Service{}
    s.NextCoreLoopback = nextLoopback(coreRange)
    s.NextPeerLoopback = nextLoopback(peerRange)
    s.NextEdgeLoopback = nextLoopback(edgeRange)

    return s, nil
}

func nextLoopback(prefix netip.Prefix) func() (netip.Addr, error) {
    ip := prefix.Addr().Prev()
    return func() (netip.Addr, error) {
        ip = ip.Next()
        if !prefix.Contains(ip) {
            return netip.Addr{}, fmt.Errorf("no available ip in %v", prefix)
        }
        return ip, nil
    }
}

The code above is the prefix package in my project. When we create a new prefix.Service via NewService(), we load the nextLoopback() function three times, once per field in our Service struct.

The nextLoopback() function contains an inner anonymous function - called a closure - that fetches and returns the next IP-address in the IP-range. By using this setup we ensure a new IP-address is returned every time the function is called.

This is all possible thanks to the nextLoopback() function getting called when the service is instantiated, keeping it "open" during the lifetime of the service. This is why we store the ip variable in the outer function, allowing the closure to run ip.Next() when called. The code block below show how to instantiate and use the service.

main.go:

package main

import (
    "confgen/internal/prefix"
    "fmt"
    "os"
)

func main() {

    pfx, err := prefix.NewService()
    if err != nil {
        fmt.Println("prefix.NewService:", err)
        os.Exit(1)
    }

    var ip netip.Addr
    ip, _ = pfx.NextCoreLoopback() // Returns 10.0.0.0
    ip, _ = pfx.NextCoreLoopback() // Returns 10.0.0.1

    ip, _ = pfx.NextPeerLoopback() // Returns 10.0.1.0
    ip, _ = pfx.NextPeerLoopback() // Returns 10.0.1.1

    fmt.Println(ip)
}

I skipped the error handling for brevity

We can see how main.go instantiates the service as pfx and how calling the different struct field functions generate a new IP-address every time.

I thought this was such a nice and clean solution that I wanted to share it. The wacky part for me is the fact that nextLoopback() returns the closure (inner function), which is why pfx.NextCoreLoopback() returns an IP-address. While I can't really say I fully understand this code, I found it to be really neat.


Copyright 2021-2025, Emil Boklund.
All Rights Reserved.